/*
 * JSWrapper.java - ScriptME
 * 
 * Copyright (c) 2009 Cesar Henriques <cesar at alttab.com.ar>.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser Public License v2.1
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Based on FESI Project
 * 
 * Contributors:
 * 	Jean-Marc Lugrin - initial API and implementation
 * 	Cesar Henriques <cesar at alttab.com.ar> - J2ME Porting and Extensions
 */
package org.scriptme.ecmascript.data;

import java.io.Reader;

import org.scriptme.ecmascript.exceptions.EcmaScriptException;
import org.scriptme.ecmascript.exceptions.ProgrammingError;
import org.scriptme.ecmascript.interpreter.Evaluator;
import org.scriptme.ecmascript.interpreter.UserEvaluationSource;
import org.scriptme.ecmascript.jslib.JSException;
import org.scriptme.ecmascript.jslib.JSFunction;
import org.scriptme.ecmascript.jslib.JSGlobalObject;
import org.scriptme.ecmascript.jslib.JSObject;

/**
 * Package an EcmaScript object as a JSObject for use by the Netscape like
 * interface.
 */
public class JSWrapper implements JSObject {

	/** The evaluator. */
	protected Evaluator evaluator;

	/** The object. */
	protected ESObject object;

	/**
	 * Create a JSWraper for an EcmaScript object.
	 * 
	 * @param object
	 *            the EcmaScript object
	 * @param evaluator
	 *            theEvaluator
	 */
	public JSWrapper(ESObject object, Evaluator evaluator) {
		super();
		this.object = object;
		this.evaluator = evaluator;
	}

	/**
	 * Gets the eS object.
	 * 
	 * @return the eS object
	 */
	public ESObject getESObject() {
		return object;
	}

	/**
	 * Return the global object attached to this object.
	 * 
	 * @return the global object
	 */
	public JSGlobalObject getGlobalObject() {
		return new JSGlobalWrapper(evaluator.getGlobalObject(), evaluator);
	}

	/**
	 * Implements the call the specified EcmaScript method of this object.
	 * 
	 * @param methodName
	 *            the method name
	 * @param args
	 *            the args
	 * 
	 * @return The result of the evaluation
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object call(String methodName, Object args[]) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				ESValue function = object.getProperty(methodName, methodName
						.hashCode());
				ESValue[] esargs = null;
				if (args == null) {
					esargs = new ESValue[0];
				} else {
					esargs = new ESValue[args.length];
					for (int i = 0; i < args.length; i++) {
						esargs[i] = ESLoader.normalizeValue(args[i], evaluator);
					}
				}
				ESValue value = function.callFunction(object, esargs); // should
																		// never
																		// return
																		// null
				obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Implements the evaluation of a string with this object as the 'this'
	 * object. The string is considered a main program (top level return is not
	 * allowed)
	 * 
	 * @param s
	 *            the s
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object eval(String s) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				ESValue value = evaluator.evaluate(s, object, false); // Can
																		// return
																		// null
																		// !
				if (value != null)
					obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Evaluate a Reader stream with this object as the 'this' object. Consider
	 * the stream being a main program, not allowing the return statement.
	 * 
	 * @param r
	 *            the r
	 * @param d
	 *            the d
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object eval(Reader r, String d) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				UserEvaluationSource ses;
				if (d == null) {
					ses = new UserEvaluationSource("<Anonymous stream>", null);
				} else {
					ses = new UserEvaluationSource(d, null);
				}
				ESValue value = evaluator.evaluate(r, object, ses, false);
				if (value != null)
					obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Evaluate a Reader stream with this object as the 'this' object. Consider
	 * the stream being a function program, allowing the return statement.
	 * 
	 * @param r
	 *            the r
	 * @param d
	 *            the d
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object evalAsFunction(Reader r, String d) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				UserEvaluationSource ses;
				if (d == null) {
					ses = new UserEvaluationSource("<Anonymous stream>", null);
				} else {
					ses = new UserEvaluationSource(d, null);
				}
				ESValue value = evaluator.evaluate(r, object, ses, true);
				if (value != null)
					obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Implements the evaluation of a string with this object as the 'this'
	 * object. The string is considered a function (top level return are
	 * allowed) Passing the specified parameters (names and values must have the
	 * same length)
	 * 
	 * @param s
	 *            the s
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object evalAsFunction(String s) throws JSException {

		Object obj = null;
		synchronized (evaluator) {
			try {
				ESValue value = evaluator.evaluate(s, object, true); // Can
																		// return
																		// null
																		// !
				if (value != null)
					obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
		/*
		 * // This work but is less efficient evalAsFunction(s, null, null);
		 */
	}

	/**
	 * Evaluate a Reader stream with this object as the 'this' object. Consider
	 * the stream being a function program, allowing the return statement.
	 * Passing the specified parameters (names and values must have the same
	 * length)
	 * 
	 * @param r
	 *            the r
	 * @param d
	 *            the d
	 * @param names
	 *            the names
	 * @param values
	 *            the values
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object evalAsFunction(Reader r, String d, String[] names,
			Object values[]) throws JSException {
		Object obj = null;
		throw new ProgrammingError("NOT IMPLEMENTED");
		/*
		 * synchronized (evaluator) { try { UserEvaluationSource ses; if
		 * (d==null) { ses = new UserEvaluationSource("<Anonymous stream>",
		 * null); } else { ses = new UserEvaluationSource(d, null); } ESValue
		 * value = evaluator.evaluate(r, object, ses, true); if (value != null)
		 * obj = value.toJavaObject(); } catch (EcmaScriptException e) { throw
		 * new JSException (e.getMessage(), e); } } return obj;
		 */
	}

	/**
	 * Implements the evaluation of a string with this object as the 'this'
	 * object. The string is considered a function (top level return are
	 * allowed)
	 * 
	 * @param body
	 *            the body
	 * @param names
	 *            the names
	 * @param values
	 *            the values
	 * 
	 * @return The result of the evaluation (null if no value returned)
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object evalAsFunction(String body, String[] names, Object values[])
			throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				// Create function
				int argLength = (names == null ? 0 : names.length);
				int checkLength = (values == null ? 0 : names.length);
				if (argLength != checkLength) {
					throw new JSException(
							"argument names and values arrays must have the same length, now: "
									+ argLength + ", " + checkLength);
				}
				ESValue esArgs[] = new ESValue[argLength + 1]; // space for
																// body
				for (int i = 0; i < argLength; i++) {
					esArgs[i] = new ESString(names[i]);
				}
				esArgs[argLength] = new ESString(body); // body is the last
														// value
				ESObject fo = evaluator.getFunctionObject();
				ESObject theFunction = fo.doConstruct(null, esArgs);
				// Now call function
				esArgs = new ESValue[argLength]; // just what is needed
				for (int i = 0; i < argLength; i++) {
					esArgs[i] = ESLoader.normalizeValue(values[i],
							this.evaluator);
				}
				ESValue value = theFunction.callFunction(object, esArgs);
				if (value != null)
					obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Implements the get named property of this object.
	 * 
	 * @param name
	 *            the name
	 * 
	 * @return The value of the property
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object getMember(String name) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				ESValue value = object.getProperty(name, name.hashCode());
				obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	/**
	 * Implement the get indexed property of this object (useful for arrays).
	 * 
	 * @param index
	 *            the index
	 * 
	 * @return The value of the property
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public Object getSlot(int index) throws JSException {
		Object obj = null;
		synchronized (evaluator) {
			try {
				ESValue value = object.getProperty(index);
				obj = value.toJavaObject();
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return obj;
	}

	// This Netscape function is not implemented
	// public static JSObject getWindow(Applet applet) throws JSException;

	/**
	 * Implement the deletion of a named property of this object.
	 * 
	 * @param name
	 *            the name
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public void removeMember(String name) throws JSException {
		synchronized (evaluator) {
			try {
				object.deleteProperty(name, name.hashCode());
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		// return;
	}

	/**
	 * Implements the set value of a named property of this object.
	 * 
	 * @param name
	 *            the name
	 * @param value
	 *            the value
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public void setMember(String name, Object value) throws JSException {
		synchronized (evaluator) {
			try {
				ESValue esvalue = ESLoader.normalizeValue(value, evaluator);
				object.putProperty(name, esvalue, name.hashCode());
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return;
	}

	/**
	 * Implement the set property by index value. Useful for arrays.
	 * 
	 * @param index
	 *            the index
	 * @param value
	 *            the value
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during interpretation
	 */
	public void setSlot(int index, Object value) throws JSException {
		synchronized (evaluator) {
			try {
				ESValue esvalue = ESLoader.normalizeValue(value, evaluator);
				object.putProperty(index, esvalue);
			} catch (EcmaScriptException e) {
				throw new JSException(e.getMessage(), e);
			}
		}
		return;
	}

	/**
	 * Implement the creation of a new evaluator, with no extension loaded.
	 * 
	 * @return The global object of the created evaluator.
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during initialization
	 */
	static public JSGlobalObject makeEvaluator() throws JSException {
		Evaluator evaluator = new Evaluator();
		GlobalObject go = evaluator.getGlobalObject();
		return new JSGlobalWrapper(go, evaluator);
	}

	/**
	 * Implement the creation of a new evaluator, with specfied extensions
	 * loaded.
	 * 
	 * @param extensions
	 *            the extensions
	 * 
	 * @return The global object of the created evaluator.
	 * 
	 * @throws JSException
	 *             the JS exception
	 * 
	 * @exception JSException
	 *                For any error during initialization
	 */
	static public JSGlobalObject makeEvaluator(String[] extensions)
			throws JSException {
		Evaluator evaluator = new Evaluator();
		GlobalObject go = evaluator.getGlobalObject();
		try {
			if (extensions != null) {
				for (int i = 0; i < extensions.length; i++) {
					Object e = evaluator.addMandatoryExtension(extensions[i]);
					if (e == null) { // Should never happens
						throw new JSException("Could not load extension '"
								+ extensions[i] + "'");
					}
				}
			}
			return new JSGlobalWrapper(go, evaluator);
		} catch (EcmaScriptException e) {
			throw new JSException(e.getMessage(), e);
		}
	}

	/**
	 * Create a built-in function object from a JSFunction object, so that it
	 * can be called as a standard function by native objects. Parameters are
	 * transformed in JSobjects if possible Java primitives are are left
	 * unhacnged and FESI primitives are transformed to Java primitives.
	 * 
	 * @param evaluator
	 *            the Evaluator
	 * @param jsf
	 *            The function to wrap
	 * 
	 * @return the ES object
	 */
	static public ESObject wrapJSFunction(Evaluator evaluator, JSFunction jsf) {
		synchronized (evaluator) {
			final JSFunction theFunction = jsf;
			class WrapedJSFunction extends BuiltinFunctionObject {
				WrapedJSFunction(String name, Evaluator evaluator,
						FunctionPrototype fp) {
					super(fp, evaluator, name, 1);
				}

				public ESValue callFunction(ESObject thisObject,
						ESValue[] arguments) throws EcmaScriptException {
					ESValue value = ESUndefined.theUndefined;
					Object jsArguments[] = new Object[arguments.length];
					for (int i = 0; i < arguments.length; i++) {
						if (arguments[i] instanceof ESWrapper) {
							jsArguments[i] = ((ESWrapper) arguments[i])
									.getJavaObject();
						} else if (arguments[i] instanceof ESObject) {
							jsArguments[i] = new JSWrapper(
									(ESObject) arguments[i], this.evaluator);
						} else {
							jsArguments[i] = arguments[i].toJavaObject();
						}
					}
					try {
						Object result = theFunction.doCall(new JSWrapper(
								thisObject, this.evaluator), jsArguments);
						value = ESLoader.normalizeValue(result, this.evaluator);
					} catch (JSException e) {
						throw new EcmaScriptException(e.getMessage());
					}
					return value;
				}

				public ESObject doConstruct(ESObject thisObject,
						ESValue[] arguments) throws EcmaScriptException {
					ESObject value = null;
					Object jsArguments[] = new Object[arguments.length];
					for (int i = 0; i < arguments.length; i++) {
						if (arguments[i] instanceof ESWrapper) {
							jsArguments[i] = ((ESWrapper) arguments[i])
									.getJavaObject();
						} else if (arguments[i] instanceof ESObject) {
							jsArguments[i] = new JSWrapper(
									(ESObject) arguments[i], this.evaluator);
						} else {
							jsArguments[i] = arguments[i].toJavaObject();
						}
					}
					try {
						Object result = theFunction.doNew(new JSWrapper(
								thisObject, this.evaluator), jsArguments);
						value = ESLoader
								.normalizeObject(result, this.evaluator);
					} catch (JSException e) {
						throw new EcmaScriptException(e.getMessage());
					}
					return value;
				}
			}
			return new WrapedJSFunction(jsf.toString(), evaluator,
					(FunctionPrototype) evaluator.getFunctionPrototype());
		}
	}

	/**
	 * Display the string value of the contained object.
	 * 
	 * @return The string value
	 */
	public String toString() {
		return object.toString();
	}
}
